package dbPhase.hypeerweb;

import java.util.ArrayList;
import java.util.HashSet;

/**
 * Node contains information for the node, but also has all the connections needed
 * to connect a Hypeerweb.
 *
 * Domain
 *  webId - WebId
 *  neighbors - HashSet<Node>
 *  upPointers - HashSet<Node>
 *  downPointers - HashSet<Node>
 *  fold - Node
 *  surrogateFold - Node
 *  inverseSurrogateFold - Node
 *  nodeState - NodeState
 *
 *  MAX_HEIGHT - int = 31
 *  NULL_NODE - Node - that extends node so you do not have to check if the node is null.
 * @author scott44
 *
 */
public class Node {

	public WebId webId;
	public HashSet<Node> neighbors;
	public HashSet<Node> upPointers;
	public HashSet<Node> downPointers;
	public Node fold;
	public Node surrogateFold;
	public Node inverseSurrogateFold;
	public NodeState nodeState;
	public Contents contents;

	private final int MAX_HEIGHT = 31;

	public static Node NULL_NODE = Null_Node.getSingleton();

	/**
	 * Node - constructor for the node class that constructs a new node when given an int.
	 *
	 * @pre id>=0
	 *
	 * @param id - The integer to use as the value to make the WebId for the node.
	 */
	public Node(int id) {
		assert (id >= 0);
		webId = new WebId(id);
		neighbors = new HashSet<Node>();
		upPointers = new HashSet<Node>();
		downPointers = new HashSet<Node>();
		fold = this;
		surrogateFold = NULL_NODE;
		inverseSurrogateFold = NULL_NODE;
		nodeState = HypercubeCapState.getSingleton(); //Do this logic elsewhere
		contents = new Contents();
	}

	/**
	 * Node - constructor for the node class that constructs a new node when given an int.
	 *
	 * @pre id>=0 height = the number of bits if the integer was binary (includes leading zeros)
	 *
	 * @param id - The integer to use as the value to make the WebId for the node.
	 * @param height - the height of the node
	 */
	public Node(int id, int height) {
		assert (id >= 0 && height <= MAX_HEIGHT
				&& height >= WebId.locationOfMostSignificantOneBit(id) + 1);

		webId = new WebId(id, height);
		neighbors = new HashSet<Node>();
		upPointers = new HashSet<Node>();
		downPointers = new HashSet<Node>();
		fold = this;
		surrogateFold = NULL_NODE;
		inverseSurrogateFold = NULL_NODE;
		nodeState = HypercubeCapState.getSingleton(); //Do this logic elsewhere
		contents = new Contents();
	}

	public void addDownPointer(Node downPointer) {
		downPointers.add(downPointer);
	}

	public void addNeighbor(Node neighbor) {
		neighbors.add(neighbor);
	}

	public void addFold(Node newFold) {
		fold = newFold;
		if (fold.getWebId().getValue() == 0) {
			nodeState = HypercubeCapState.getSingleton();
		}
		else {
			nodeState = TerminalNodeState.getSingleton();
		}
	}

	public void addUpPointer(Node upPointer) {
		upPointers.add(upPointer);
	}

	/**
	 * constructSimplifiedNodeDomain constructs a simplified node from this node.
	 *
	 * @pre none
	 * @return the node representation in as a SimplifiedNodeDomain
	 */
	public SimplifiedNodeDomain constructSimplifiedNodeDomain() {
		HashSet<Integer> intNeighbors = new HashSet<Integer>();
		for (Node n:neighbors) {
			intNeighbors.add(n.getWebId().getValue());
		}

		HashSet<Integer> intUpPointers = new HashSet<Integer>();
		for (Node n:upPointers) {
			intUpPointers.add(n.getWebId().getValue());
		}

		HashSet<Integer> intDownPointers = new HashSet<Integer>();
		for (Node n:downPointers) {
			intDownPointers.add(n.getWebId().getValue());
		}

		SimplifiedNodeDomain simpleNode = new SimplifiedNodeDomain(
				webId.getValue(),
				webId.getHeight(),
				intNeighbors,
				intUpPointers,
				intDownPointers,
				(fold == null ? -1 : fold.getWebId().getValue()),
				(surrogateFold == null ? -1 : surrogateFold.getWebId().getValue()),
				(inverseSurrogateFold == null ? -1 : inverseSurrogateFold.getWebId().getValue()), nodeState.getStateId());

		return simpleNode;
	}

//	public dbPhase.spec.Connections getConnections() {
//
//		return null;
//	}

	public WebId getWebId() {
		return webId;
	}

	public void removeDownPointer(Node downPointer) {
		downPointers.remove(downPointer);
	}

	public void removeNeighbor(Node neighbor) {
		neighbors.remove(neighbor);
	}

	public void removeUpPointer(Node upPointer) {
		upPointers.remove(upPointer);
	}

//	public void setConnections(dbPhase.spec.Connections connections) {
//
//	}

	public void setFold(Node newFold) {
		fold = newFold;
	}

	public void setInverseSurrogateFold(Node newInverseSurrogateFold) {
		inverseSurrogateFold = newInverseSurrogateFold;
	}

	public void setSurrogateFold(Node newSurrogateFold) {
		surrogateFold = newSurrogateFold;
	}

	public void setWebId(WebId id) {
		this.webId = id;
	}

	/*------------------------------Phase 2----------------------------------------*/
	// uses me as the start node
	/**
	 * addToHyPeerWeb does just that takes in a node and adds it to the existing HyPeerWeb
	 *
	 * @pre newNode != null
	 *
	 * @param newNode - the node to add to the Hyperweb
	 */
	public void addToHyPeerWeb(Node newNode) {
		AddOrRemoveNode add = new AddNode();
		add.execute(this, newNode);
	}

	private class AddNode extends AddOrRemoveNode {
		AddNode() {

		}
		@Override
		public Node findPoint(Node terminal) {
			if (terminal.nodeState == HypercubeCapState.getSingleton()) {
				return terminal.fold;
			}
			return terminal.nodeState.search(terminal.getSmallestNeighbor().getWebIdValue(),
					terminal.getSmallestDownPointer().getWebIdValue(), terminal);
		}
		@Override
		public void makeChanges(Node point, Node change) {
			point.addChild(change);
		}
	}

	/**
	 * addChild uses the newNode to add as a child once the terminal node is found to know where to add the
	 * newNode
	 *
	 * @pre newNode is not null
	 * @post where the terminal node was found that nodes child becomes the new node.
	 * @param newNode - the newNode to be added as a child.
	 */
	public void addChild(Node newNode) {

		//change newnode
		newNode.setWebId(new WebId(webId.getValue() + (int)Math.pow(2.0,webId.getHeight()),
				webId.getHeight() + 1));

		webId = new WebId(webId.getValue(), webId.getHeight() + 1);

		ArrayList<Connection> connections = new ArrayList<Connection>();
		NeighborRole me = new NeighborRole(this, this);
		connections.add(me);

		for (Node n: upPointers) {
			NeighborRole neighbor = new NeighborRole(n, this);
			connections.add(neighbor);
		}

		for (Node n: neighbors) {
			if (n.getWebIdValue() == newNode.getWebIdValue()) {
				continue;
			}
			if (webId.getValue() < n.getWebIdValue()) {
				UpPointerRole uppointer = new UpPointerRole(n, this);
				connections.add(uppointer);
			}
		}

		upPointers = new HashSet<Node>();  // remove my connections

		//Change state to standard
		nodeState = StandardNodeState.getSingleton();

		///FOLD
		FoldRole foldr = null;
		if (fold == this) {
			foldr = new FoldRole(this, this);
			fold = NULL_NODE;
			inverseSurrogateFold = NULL_NODE;
			surrogateFold = NULL_NODE;
		}
		else if (inverseSurrogateFold != NULL_NODE) {
			foldr = new FoldRole(inverseSurrogateFold, this);
			inverseSurrogateFold = NULL_NODE;
		}
		else {
			foldr = new FoldRole(fold, this);
			surrogateFold = fold;
			fold = NULL_NODE;
		}
		connections.add(foldr);
		newNode.setConnections(connections);
		HyPeerWeb.getSingleton().addNode(newNode);
	}

	/**
	 *
	 * @return simply gets a nodes biggest neighbor
	 */
	public Node getBiggestNeighbor() {
		Node biggest = Null_Node.getSingleton();
		for (Node n: this.neighbors) {
			if (n.getWebId().getValue() > biggest.getWebId().getValue()) {
				biggest = n;
			}
		}

		return biggest;
	}

	/**
	 *
	 * @return simply gets a nodes biggest up pointer
	 */
	public Node getBiggestUpPointer() {
		Node biggest = Null_Node.getSingleton();
		for (Node n: this.upPointers) {
			if (n.getWebId().getValue() > biggest.getWebId().getValue()) {
				biggest = n;
			}
		}

		return biggest;
	}

	/**
	 *
	 * @return simply gets a nodes biggest down pointer
	 */
	public Node getBiggestDownPointer() {
		Node biggest = Null_Node.getSingleton();
		for (Node n: this.downPointers) {
			if (n.getWebId().getValue() > biggest.getWebId().getValue()) {
				biggest = n;
			}
		}

		return biggest;
	}

	/**
	 *
	 * @return simply gets a nodes smallest neighbor
	 */
	public Node getSmallestNeighbor() {
		Node smallest = Null_Node.getSingleton();
		for (Node n: this.neighbors) {
			if (smallest == Null_Node.getSingleton()) {
				smallest = n;
			}

			if (n.getWebId().getValue() < smallest.getWebId().getValue()) {
				smallest = n;
			}
		}

		return smallest;
	}

	/**
	 *
	 * @return simply gets a nodes smallest up pointer
	 */
	public Node getSmallestUpPointer() {
		Node smallest = Null_Node.getSingleton();
		for (Node n: this.upPointers) {
			if (smallest == Null_Node.getSingleton()) {
				smallest = n;
			}

			if (n.getWebId().getValue() < smallest.getWebId().getValue()) {
				smallest = n;
			}
		}

		return smallest;
	}

	/**
	 *
	 * @return simply gets a nodes smallest down pointer
	 */
	public Node getSmallestDownPointer() {
		Node smallest = Null_Node.getSingleton();
		for (Node n: this.downPointers) {
			if (smallest == Null_Node.getSingleton()) {
				smallest = n;
			}

			if (n.getWebId().getValue() < smallest.getWebId().getValue()) {
				smallest = n;
			}
		}

		return smallest;
	}

	/*------------------------------Phase 3----------------------------------------*/

	public int getWebIdValue() {
		return this.getWebId().getValue();
	}

	/**
	 * removeFromHyPeerWeb removes the node that called the function from the HyPeerWeb that it belongs to.
	 *
	 * @pre the node that calls the method actually belongs to a HyPeerWeb
	 * @post the node will be remove with no connections and the last node in the HyPeerWeb will take its spot
	 * with all of its former connections.
	 *
	 */
	public void removeFromHyPeerWeb() {

		int id = this.getWebIdValue();
		
		if(id>=0){///~~~~~~~~~~
			Node dp = this.nodeState.findDP(this);
			Node toDelete = this;
	
			// remove dp from hypeerweb
			UpdateConnections remover = new UpdateConnections(dp, Node.NULL_NODE) {
				@Override
				public void makeChanges(Connection c) {
					c.removeConnection(conSource);
				}
			};
			remover.execute();
			int height = this.getWebId().getHeight();
			NodeState removedstate = this.nodeState;
			dp.nodeState = removedstate;
	
			// replace connection
			UpdateConnections replacer = new UpdateConnections(toDelete, dp) {
				@Override
				public void makeChanges(Connection c) {
					c.replaceConnection(conSource, conDest);
				}
			};
			replacer.execute();
			HyPeerWeb.getSingleton().removeNode(this);
			dp.setWebId(new WebId(id, height));
			HyPeerWeb.getSingleton().nodes.add(id, dp);
	
			// remove dp from top of ArrayList, dont want duplicates.
			HyPeerWeb.getSingleton().nodes.remove(HyPeerWeb.getSingleton().nodes.size() - 1);
	
			if (dp.getWebIdValue() == 0 && dp.neighbors.size() == 0 && dp.downPointers.size() == 0 && dp.upPointers.size() == 0) {
				dp.fold = dp;
				dp.inverseSurrogateFold = Node.NULL_NODE;
				dp.surrogateFold = Node.NULL_NODE;
				dp.nodeState = HypercubeCapState.getSingleton();
			}
		}//~~~~~~~~

	}

	/**
	 *
	 * @return simply gets all the connections of a given node
	 */
	public ArrayList<Connection> getConnections() {
		ArrayList<Connection> connections = new ArrayList<Connection>();
		for (Node n: neighbors) {
			connections.add(new NeighborRole(n, this.getSmallestNeighbor()));
		}
		for (Node n: upPointers) {
			connections.add(new UpPointerRole(n, this.getSmallestNeighbor()));
		}
		for (Node n: downPointers) {
			connections.add(new DownPointerRole(n, this.getSmallestNeighbor()));
		}
		if (fold != NULL_NODE) {
			connections.add(new FoldRole(fold, this.getSmallestNeighbor()));
		}
		if (surrogateFold != NULL_NODE) {
			connections.add(new SurrogateFoldRole(surrogateFold, this.getSmallestNeighbor()));
		}
		if (inverseSurrogateFold != NULL_NODE) {
			connections.add(new InverseSurrogateFoldRole(inverseSurrogateFold, this.getSmallestNeighbor()));
		}

		return connections;
	}

	/**
	 *
	 * @param connections - simply sets these as the nodes connections.
	 */
	public void setConnections(ArrayList<Connection> connections) {
		// --- This will eventually be replaced by the Template Pattern
		for (Connection c: connections) {
			if (c instanceof NeighborRole) {
				addNeighbor(c.delegate);
			}
			if (c instanceof UpPointerRole) {
				addDownPointer(c.delegate);
			}
			if (c instanceof FoldRole) {
				addFold(c.delegate);
			}
			c.addConnection(this);
		}
	}

	/*------------------------------Phase 4----------------------------------------*/

	/*The accept method for the visitor Pattern.
Pre-condition:
visitor ≠ null AND parameters ≠ null AND visitor.visit(this, parameters).pre-condition
Post-condition:
visitor.visit(this, parameters).post-condition*/
	public void accept(Visitor visitor, Parameters parameters) {
		visitor.visit(this, parameters);
	}

	/*Get's this node's contents.
Pre-condition:
None
Post-condition:
result = contents*/
	public Contents getContents() {
		return contents;
	}

//	public ConnectionContainer getConnectionsContainer() {
//		return new ConnectionContainer(this.getConnections());
//	}
}
